/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.dw.library.widget;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.GestureDetector;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Transformation;
import android.widget.Gallery;

import com.iss.library.R;

/**
 * A view that shows items in a center-locked, horizontally scrolling list.
 * <p>
 * The default values for the Gallery assume you will be using
 * {@link android.R.styleable#Theme_galleryItemBackground} as the background for
 * each View given to the Gallery from the Adapter. If you are not doing this,
 * you may need to adjust some Gallery properties, such as the spacing.
 * <p>
 * Views given to the Gallery should use {@link Gallery.LayoutParams} as their
 * layout parameters type.
 * 
 * <p>
 * See the <a href="{@docRoot}
 * resources/tutorials/views/hello-gallery.html">Gallery tutorial</a>.
 * </p>
 * 
 * @attr ref android.R.styleable#Gallery_animationDuration
 * @attr ref android.R.styleable#Gallery_spacing
 * @attr ref android.R.styleable#Gallery_gravity
 */
public class Carousel extends AbsCarouselSpinner implements GestureDetector.OnGestureListener {

  private static final String TAG = "Gallery";

  private static final boolean localLOGV = false;
  
  private static final int TAG_KEY = R.string.tag_key;

  /**
   * 
   */
  private static final float DEF_GRADIENT = 25.0f;

  /**
   * 弧度
   */
  private static final float RADIAN = (float) (Math.PI) / 180.0f;

  /**
   * 圆盘直径
   */
  private int mDiameter = 0;

  /**
   * 圆盘与水平面夹角
   */
  private float mGradient = DEF_GRADIENT * RADIAN;

  /**
   * 圆盘有效范围(坐标区域)高
   */
  private int mValidHeight = 0;

  /**
   * 圆盘有效范围(坐标区域)宽
   */
  private int mValidWidth = 0;

  /**
   * 圆盘有效范围(坐标区域) 与控件边缘的距离
   */
  private Rect mValidPadding = new Rect();

  /**
   * 
   */
  private int mHeightMeasureSpec;

  /**
   * 
   */
  private int mWidthMeasureSpec;

  /**
   * Duration in milliseconds from the start of a scroll during which we're
   * unsure whether the user is scrolling or flinging.
   */
  private static final int SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT = 250;

  /**
   * How long the transition animation should run when a child view changes
   * position, measured in milliseconds.
   */
  private int mAnimationDuration = 400;

  /**
   * Helper for detecting touch gestures.
   */
  private GestureDetector mGestureDetector;

  /**
   * The position of the item that received the user's down touch.
   */
  private int mDownTouchPosition;

  /**
   * The view of the item that received the user's down touch.
   */
  private View mDownTouchView;

  /**
   * Executes the delta scrolls from a fling or scroll movement.
   */
  private FlingRotateRunnable mFlingRunnable = new FlingRotateRunnable();

  /**
   * Sets mSuppressSelectionChanged = false. This is used to set it to false in
   * the future. It will also trigger a selection changed.
   */
  private Runnable mDisableSuppressSelectionChangedRunnable = new Runnable() {
    public void run() {
      mSuppressSelectionChanged = false;
      selectionChanged();
    }
  };

  /**
   * When fling runnable runs, it resets this to false. Any method along the
   * path until the end of its run() can set this to true to abort any remaining
   * fling. For example, if we've reached either the leftmost or rightmost item,
   * we will set this to true.
   */
  private boolean mShouldStopFling;

  /**
   * The currently selected item's child.
   */
  private View mSelectedChild;

  /**
   * Whether to continuously callback on the item selected listener during a
   * fling.
   */
  private boolean mShouldCallbackDuringFling = true;

  /**
   * Whether to callback when an item that is not selected is clicked.
   */
  private boolean mShouldCallbackOnUnselectedItemClick = true;

  /**
   * If true, do not callback to item selected listener.
   */
  private boolean mSuppressSelectionChanged;

  /**
   * If true, we have received the "invoke" (center or enter buttons) key down.
   * This is checked before we action on the "invoke" key up, and is
   * subsequently cleared.
   */
  private boolean mReceivedInvokeKeyDown;

  private AdapterContextMenuInfo mContextMenuInfo;

  /**
   * If true, this onScroll is the first for this user's drag (remember, a drag
   * sends many onScrolls).
   */
  private boolean mIsFirstScroll;

  public Carousel(Context context) {
    this(context, null);
  }

  public Carousel(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public Carousel(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);

    mGestureDetector = new GestureDetector(context, this);
    mGestureDetector.setIsLongpressEnabled(true);

    TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.Carousel);
    int diameter = array.getDimensionPixelSize(R.styleable.Carousel_diameter, 0);
    float gradient = array.getFloat(R.styleable.Carousel_gradient, DEF_GRADIENT);
    mGradient = gradient * RADIAN;
    setDiameter(diameter);

    setStaticTransformationsEnabled(true);
    setChildrenDrawingOrderEnabled(true);
    setWillNotDraw(false);
  }

  /**
   * Whether or not to callback on any {@link #getOnItemSelectedListener()}
   * while the items are being flinged. If false, only the final selected item
   * will cause the callback. If true, all items between the first and the final
   * will cause callbacks.
   * 
   * @param shouldCallback
   *          Whether or not to callback on the listener while the items are
   *          being flinged.
   */
  public void setCallbackDuringFling(boolean shouldCallback) {
    mShouldCallbackDuringFling = shouldCallback;
  }

  /**
   * Whether or not to callback when an item that is not selected is clicked. If
   * false, the item will become selected (and re-centered). If true, the
   * {@link #getOnItemClickListener()} will get the callback.
   * 
   * @param shouldCallback
   *          Whether or not to callback on the listener when a item that is not
   *          selected is clicked.
   * @hide
   */
  public void setCallbackOnUnselectedItemClick(boolean shouldCallback) {
    mShouldCallbackOnUnselectedItemClick = shouldCallback;
  }

  /**
   * Sets how long the transition animation should run when a child view changes
   * position. Only relevant if animation is turned on.
   * 
   * @param animationDurationMillis
   *          The duration of the transition, in milliseconds.
   * 
   * @attr ref android.R.styleable#Gallery_animationDuration
   */
  public void setAnimationDuration(int animationDurationMillis) {
    mAnimationDuration = animationDurationMillis;
  }

  public int getDiameter() {
    return mDiameter;
  }

  public float getGradient() {
    return mGradient;
  }

  public void setGradient(float gradient) {
    this.mGradient = gradient;
    requestLayout();
  }

  @Override
  protected boolean getChildStaticTransformation(View child, Transformation t) {
    t.clear();
    t.setTransformationType(Transformation.TYPE_MATRIX);
    CarouselItemTag tag = (CarouselItemTag) child.getTag(TAG_KEY);
    // Center of the item
    float centerX = (float) child.getWidth() / 2, centerY = (float) child.getHeight() / 2;
    // Translate the item to it's coordinates
    final Matrix matrix = t.getMatrix();
    matrix.setScale(tag.scale, tag.scale, centerX, centerY);
    return true;
  }

  @Override
  protected int computeHorizontalScrollExtent() {
    // Only 1 item is considered to be selected
    return 1;
  }

  @Override
  protected int computeHorizontalScrollOffset() {
    // Current scroll position is the same as the selected position
    return mSelectedPosition;
  }

  @Override
  protected int computeHorizontalScrollRange() {
    // Scroll range is the same as the item count
    return mItemCount;
  }

  @Override
  protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return p instanceof LayoutParams;
  }

  @Override
  protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return new LayoutParams(p);
  }

  @Override
  public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
  }

  @Override
  protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
    /*
     * Gallery expects Gallery.LayoutParams.
     */
    return new Carousel.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize;
    int heightSize;

    if (mDataChanged) {
      handleDataChanged();
    }

    int preferredHeight = 0;
    int preferredWidth = 0;
    boolean needsMeasuring = true;

    int selectedPosition = getSelectedItemPosition();
    if (selectedPosition >= 0 && mAdapter != null && selectedPosition < mAdapter.getCount()) {
      // Try looking in the recycler. (Maybe we were measured once already)
      View view = mRecycler.get(selectedPosition);
      if (view == null) {
        // Make a new one
        view = mAdapter.getView(selectedPosition, null, this);
      }

      if (view != null) {
        // Put in recycler for re-measuring and/or layout
        mRecycler.put(selectedPosition, view);
      }

      if (null == mValidPadding) {
        mValidPadding = new Rect();
      }

      if (view != null) {
        if (view.getLayoutParams() == null) {
          mBlockLayoutRequests = true;
          view.setLayoutParams(generateDefaultLayoutParams());
          mBlockLayoutRequests = false;
        }
        measureChild(view, widthMeasureSpec, heightMeasureSpec);

        int childWidth = getChildWidth(view);
        int childHeight = getChildHeight(view);

        if (mDiameter == 0) {
          measureDiameter(childWidth, childHeight, MeasureSpec.getSize(widthMeasureSpec),
              MeasureSpec.getSize(heightMeasureSpec));
        }

        float leftScale = getScale(270);
        float topScale = getScale(180);
        float bottomScale = getScale(0);

        mValidPadding.left = (int) (getPaddingLeft() + childWidth * leftScale / 2);
        mValidPadding.top = (int) (getPaddingTop() + childHeight * topScale / 2);
        mValidPadding.right = (int) (getPaddingRight() + childWidth * leftScale / 2);
        mValidPadding.bottom = (int) (getPaddingBottom() + childHeight * bottomScale / 2);

        preferredHeight = (int) (mValidPadding.top + mValidPadding.bottom + mValidHeight);
        preferredWidth = (int) (mValidPadding.left + mValidPadding.right + mValidWidth);

        needsMeasuring = false;
      }
    }

    if (needsMeasuring) {
      mValidPadding.left = getPaddingLeft();
      mValidPadding.top = getPaddingTop();
      mValidPadding.right = getPaddingRight();
      mValidPadding.bottom = getPaddingBottom();
      // No views -- just use padding
      preferredHeight = mValidPadding.top + mValidPadding.bottom;
      if (widthMode == MeasureSpec.UNSPECIFIED) {
        preferredWidth = mValidPadding.left + mValidPadding.right;
      }
    }

    preferredHeight = Math.max(preferredHeight, getSuggestedMinimumHeight());
    preferredWidth = Math.max(preferredWidth, getSuggestedMinimumWidth());

    heightSize = resolveSize(preferredHeight, heightMeasureSpec);
    widthSize = resolveSize(preferredWidth, widthMeasureSpec);

    setMeasuredDimension(widthSize, heightSize);
    this.mWidthMeasureSpec = widthMeasureSpec;
    this.mHeightMeasureSpec = heightMeasureSpec;
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);

    /*
     * Remember that we are in layout to prevent more layout request from being
     * generated.
     */
    mInLayout = true;
    layout(0, false);
    mInLayout = false;
  }

  public void setDiameter(int diameter) {
    if (mDiameter != diameter) {
      this.mDiameter = diameter;
      mValidWidth = getValidWidth();
      mValidHeight = getValidHeight();
    }
  }

  /**
   * 
   * @param tag
   * @param position
   * @param angle
   * @param gradient
   * @return
   */
  private CarouselItemTag getCarouselItemTag(CarouselItemTag tag, int position, float angle) {
    if (null == tag) {
      tag = new CarouselItemTag();
    }
    tag.angle = angle;
    tag.scale = getScale(angle);

    angle = angle * RADIAN;
    tag.x = mValidWidth / 2 + (float) (tag.scale * Math.sin(angle) * mDiameter / 2); // x
    tag.y = mValidHeight / 2 + (float) (tag.scale * Math.cos(angle) * Math.sin(mGradient) * mDiameter / 2);

    tag.position = position;

    return tag;
  }

  private float getScale(float angle) {
    angle = angle * RADIAN;
    return (1.0f + (float) Math.cos(angle)) / 4 + 0.5f;
  }

  private void measureDiameter(int childWidth, int childHeight, int width, int height) {
    float leftScale = getScale(270);
    float topScale = getScale(180);
    float bottomScale = getScale(0);
    float w = width - getPaddingLeft() - getPaddingRight() - childWidth * leftScale;
    if (height == 0) {
      setDiameter((int) w);
      return;
    }
    float h = height - getPaddingTop() - getPaddingBottom() - childHeight * topScale / 2 + childHeight * bottomScale
        / 2;
    float hDia = (float) (h / Math.sin(mGradient));
    if (w == 0) {
      setDiameter((int) hDia);
      return;
    }
    if (w <= hDia) {
      setDiameter((int) w);
    } else {
      setDiameter((int) hDia);
    }
  }

  private int getValidWidth() {
    return mDiameter;
  }

  private int getValidHeight() {
    return (int) (Math.sin(mGradient) * mDiameter);
  }

  int getChildHeight(View child) {
    return child.getMeasuredHeight();
  }

  int getChildWidth(View child) {
    return child.getMeasuredWidth();
  }

  /**
   * Tracks a motion scroll. In reality, this is used to do just about any
   * movement to items (touch scroll, arrow-key scroll, set an item as
   * selected).
   * 
   * @param deltaAngle
   *          Change in X from the previous event.
   */
  void trackMotionScroll(float deltaAngle) {

    if (getChildCount() == 0) {
      return;
    }
    CarouselItemTag oldTag = null;
    CarouselItemTag tag = null;
    View child = null;
    for (int i = 0; i < mItemCount; i++) {
      child = getChildAt(i);
      oldTag = (CarouselItemTag) child.getTag(TAG_KEY);
      float angle = oldTag.angle;
      angle += deltaAngle;
      while (angle > 360.0f) {
        angle -= 360.0f;
      }
      while (angle < 0.0f) {
        angle += 360.0f;
      }
      tag = getCarouselItemTag(null, i, angle);
      child.setTag(TAG_KEY, tag);
    }
    // invalidate();
    requestLayout();
  }

  /**
   * Brings an item with nearest to 0 degrees angle to this angle and sets it
   * selected
   */
  private void scrollIntoSlots() {
    // Nothing to do
    if (getChildCount() == 0 || mSelectedChild == null)
      return;

    // get nearest item to the 0 degrees angle
    // Sort itmes and get nearest angle
    float angle;
    int position;

    ArrayList<View> arr = new ArrayList<View>();

    for (int i = 0; i < mItemCount; i++) {
      arr.add(getChildAt(i));
    }

    Collections.sort(arr, new Comparator<View>() {
      @Override
      public int compare(View v1, View v2) {
        float a1 = ((CarouselItemTag) v1.getTag(TAG_KEY)).angle;
        if (a1 > 180) {
          a1 = 360 - a1;
        }
        float a2 = ((CarouselItemTag) v2.getTag(TAG_KEY)).angle;
        if (a2 > 180) {
          a2 = 360 - a2;
        }
        return a1 > a2 ? 1 : -1;
      }

    });

    CarouselItemTag tag = (CarouselItemTag) arr.get(0).getTag(TAG_KEY);
    angle = tag.angle;

    // Make it minimum to rotate
    if (angle > 180.0f) {
      angle = -(360.0f - angle);
    }

    // Start rotation if needed
    if (angle != 0.0f) {
      mFlingRunnable.startUsingDistance(angle);
    } else {
      // Set selected position
      position = tag.position;
      setSelectedPositionInt(position);
      onFinishedMovement();
    }

  }

  private void onFinishedMovement() {
    if (mSuppressSelectionChanged) {
      mSuppressSelectionChanged = false;

      // We haven't been callbacking during the fling, so do it now
      super.selectionChanged();
    }
    invalidate();
  }

  @Override
  void selectionChanged() {
    if (!mSuppressSelectionChanged) {
      super.selectionChanged();
    }
  }

  /**
   * Creates and positions all views for this Gallery.
   * <p>
   * We layout rarely, most of the time {@link #trackMotionScroll(int)} takes
   * care of repositioning, adding, and removing children.
   * 
   * @param delta
   *          Change in the selected position. +1 means the selection is moving
   *          to the right, so views are scrolling to the left. -1 means the
   *          selection is moving to the left.
   */
  @Override
  void layout(int delta, boolean animate) {
    if (mDataChanged) {
      handleDataChanged();
    }

    // Handle an empty gallery by removing all views.
    if (mItemCount == 0) {
      resetList();
      return;
    }

    // Update to the new selected position.
    if (mNextSelectedPosition >= 0) {
      setSelectedPositionInt(mNextSelectedPosition);
    }

    // All views go in recycler while we are in layout
    recycleAllViews();

    // Clear out old views
    // removeAllViewsInLayout();
    detachAllViewsFromParent();

    float angleUnit = 360.0f / mItemCount;
    for (int i = 0; i < mItemCount; i++) {
      float angle = (i - mSelectedPosition) * angleUnit;
      if (angle < 0) {
        angle += 360;
      }
      makeAndAddView(i, angle);
    }

    // Flush any cached views that did not get reused above
    mRecycler.clear();

    invalidate();
    checkSelectionChanged();

    mDataChanged = false;
    mNeedSync = false;
    setNextSelectedPositionInt(mSelectedPosition);

    updateSelectedItemMetadata();
  }

  /**
   * Obtain a view, either by pulling an existing view from the recycler or by
   * getting a new one from the adapter. If we are animating, make sure there is
   * enough information in the view's layout parameters to animate from the old
   * to new positions.
   * 
   * @param position
   *          Position in the gallery for the view to obtain
   * @param offset
   *          Offset from the selected position
   * @param x
   *          X-coordintate indicating where this view should be placed. This
   *          will either be the left or right edge of the view, depending on
   *          the fromLeft paramter
   * @param fromLeft
   *          Are we posiitoning views based on the left edge? (i.e., building
   *          from left to right)?
   * @return A view that has been added to the gallery
   */
  private View makeAndAddView(int position, float angle) {

    View child = null;
    CarouselItemTag tag = null;

    if (!mDataChanged) {
      child = mRecycler.get(position);
      if (child != null) {
        tag = (CarouselItemTag) child.getTag(TAG_KEY);
        if (null == tag) {
          tag = getCarouselItemTag(null, position, angle);
        } else {
          tag = getCarouselItemTag(tag, position, tag.angle);
        }
        child.setTag(TAG_KEY, tag);
        // Position the view
        setUpChild(child);

        return child;
      } else {
        // Nothing found in the recycler -- ask the adapter for a view
        child = mAdapter.getView(position, null, this);
        tag = getCarouselItemTag(null, position, angle);
        child.setTag(TAG_KEY, tag);

        // Position the view
        setUpChild(child);

        return child;
      }
    }

    // Nothing found in the recycler -- ask the adapter for a view
    child = mAdapter.getView(position, null, this);
    tag = getCarouselItemTag(null, position, angle);
    child.setTag(TAG_KEY, tag);

    // Position the view
    setUpChild(child);

    return child;
  }

  /**
   * Helper for makeAndAddView to set the position of a view and fill out its
   * layout paramters.
   * 
   * @param child
   *          The view to position
   * @param offset
   *          Offset from the selected position
   * @param x
   *          X-coordintate indicating where this view should be placed. This
   *          will either be the left or right edge of the view, depending on
   *          the fromLeft paramter
   * @param fromLeft
   *          Are we posiitoning views based on the left edge? (i.e., building
   *          from left to right)?
   */
  private void setUpChild(View child) {
    CarouselItemTag tag = (CarouselItemTag) child.getTag(TAG_KEY);
    // Respect layout params that are already in the view. Otherwise
    // make some up...
    Carousel.LayoutParams lp = (Carousel.LayoutParams) child.getLayoutParams();
    if (lp == null) {
      lp = (Carousel.LayoutParams) generateDefaultLayoutParams();
    }

    addViewInLayout(child, -1, lp);

    child.setSelected(tag.position == mSelectedPosition);

    // // Get measure specs
    int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec, 0, lp.height);
    int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, 0, lp.width);

    // Measure child
    child.measure(childWidthSpec, childHeightSpec);

    int width = child.getMeasuredWidth();
    int height = child.getMeasuredHeight();
    int left = (int) (mValidPadding.left + tag.x - width / 2.0f);
    int top = (int) (mValidPadding.top + tag.y - height / 2.0f);

    child.layout(left, top, left + width, top + height);
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {

    // Give everything to the gesture detector
    boolean retValue = mGestureDetector.onTouchEvent(event);

    int action = event.getAction();
    if (action == MotionEvent.ACTION_UP) {
      // Helper method for lifted finger
      onUp();
    } else if (action == MotionEvent.ACTION_CANCEL) {
      onCancel();
    }

    return retValue;
  }

  /**
   * {@inheritDoc}
   */
  public boolean onSingleTapUp(MotionEvent e) {

    if (mDownTouchPosition >= 0) {

      // An item tap should make it selected, so scroll to this child.
      scrollToChild(mDownTouchPosition - mFirstPosition);

      // Also pass the click so the client knows, if it wants to.
      if (mShouldCallbackOnUnselectedItemClick || mDownTouchPosition == mSelectedPosition) {
        performItemClick(mDownTouchView, mDownTouchPosition, mAdapter.getItemId(mDownTouchPosition));
      }

      return true;
    }

    return false;
  }

  /**
   * {@inheritDoc}
   */
  public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {

    if (!mShouldCallbackDuringFling) {
      // We want to suppress selection changes

      // Remove any future code to set mSuppressSelectionChanged = false
      removeCallbacks(mDisableSuppressSelectionChangedRunnable);

      // This will get reset once we scroll into slots
      if (!mSuppressSelectionChanged)
        mSuppressSelectionChanged = true;
    }

    // Fling the gallery!
    mFlingRunnable.startUsingVelocity((int) -velocityX);

    return true;
  }

  /**
   * {@inheritDoc}
   */
  public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {

    if (localLOGV)
      Log.v(TAG, String.valueOf(e2.getX() - e1.getX()));

    /*
     * Now's a good time to tell our parent to stop intercepting our events! The
     * user has moved more than the slop amount, since GestureDetector ensures
     * this before calling this method. Also, if a parent is more interested in
     * this touch's events than we are, it would have intercepted them by now
     * (for example, we can assume when a Gallery is in the ListView, a vertical
     * scroll would not end up in this method since a ListView would have
     * intercepted it by now).
     */
    getParent().requestDisallowInterceptTouchEvent(true);

    // As the user scrolls, we want to callback selection changes so related-
    // info on the screen is up-to-date with the gallery's selection
    if (!mShouldCallbackDuringFling) {
      if (mIsFirstScroll) {
        /*
         * We're not notifying the client of selection changes during the fling,
         * and this scroll could possibly be a fling. Don't do selection changes
         * until we're sure it is not a fling.
         */
        if (!mSuppressSelectionChanged)
          mSuppressSelectionChanged = true;
        postDelayed(mDisableSuppressSelectionChangedRunnable, SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT);
      }
    } else {
      if (mSuppressSelectionChanged)
        mSuppressSelectionChanged = false;
    }

    // Track the motion
    trackMotionScroll((int) -distanceX);

    mIsFirstScroll = false;
    return true;
  }

  /**
   * {@inheritDoc}
   */
  public boolean onDown(MotionEvent e) {

    // Kill any existing fling/scroll
    mFlingRunnable.stop(false);

    // Get the item's view that was touched
    mDownTouchPosition = pointToPosition((int) e.getX(), (int) e.getY());

    if (mDownTouchPosition >= 0) {
      mDownTouchView = getChildAt(mDownTouchPosition - mFirstPosition);
      mDownTouchView.setPressed(true);
    }

    // Reset the multiple-scroll tracking state
    mIsFirstScroll = true;

    // Must return true to get matching events for this down event.
    return true;
  }

  /**
   * Called when a touch event's action is MotionEvent.ACTION_UP.
   */
  void onUp() {

    if (mFlingRunnable.mRotator.isFinished()) {
      scrollIntoSlots();
    }

    dispatchUnpress();
  }

  /**
   * Called when a touch event's action is MotionEvent.ACTION_CANCEL.
   */
  void onCancel() {
    onUp();
  }

  /**
   * {@inheritDoc}
   */
  public void onLongPress(MotionEvent e) {

    if (mDownTouchPosition < 0) {
      return;
    }

    performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
    long id = getItemIdAtPosition(mDownTouchPosition);
    dispatchLongPress(mDownTouchView, mDownTouchPosition, id);
  }

  // Unused methods from GestureDetector.OnGestureListener below

  /**
   * {@inheritDoc}
   */
  public void onShowPress(MotionEvent e) {
  }

  // Unused methods from GestureDetector.OnGestureListener above

  private void dispatchPress(View child) {

    if (child != null) {
      child.setPressed(true);
    }

    setPressed(true);
  }

  private void dispatchUnpress() {

    for (int i = getChildCount() - 1; i >= 0; i--) {
      getChildAt(i).setPressed(false);
    }

    setPressed(false);
  }

  @Override
  public void dispatchSetSelected(boolean selected) {
    /*
     * We don't want to pass the selected state given from its parent to its
     * children since this widget itself has a selected state to give to its
     * children.
     */
  }

  @Override
  protected void dispatchSetPressed(boolean pressed) {

    // Show the pressed state on the selected child
    if (mSelectedChild != null) {
      mSelectedChild.setPressed(pressed);
    }
  }

  @Override
  protected ContextMenuInfo getContextMenuInfo() {
    return mContextMenuInfo;
  }

  @Override
  public boolean showContextMenuForChild(View originalView) {

    final int longPressPosition = getPositionForView(originalView);
    if (longPressPosition < 0) {
      return false;
    }

    final long longPressId = mAdapter.getItemId(longPressPosition);
    return dispatchLongPress(originalView, longPressPosition, longPressId);
  }

  @Override
  public boolean showContextMenu() {

    if (isPressed() && mSelectedPosition >= 0) {
      int index = mSelectedPosition - mFirstPosition;
      View v = getChildAt(index);
      return dispatchLongPress(v, mSelectedPosition, mSelectedRowId);
    }

    return false;
  }

  private boolean dispatchLongPress(View view, int position, long id) {
    boolean handled = false;

    if (mOnItemLongClickListener != null) {
      handled = mOnItemLongClickListener.onItemLongClick(this, mDownTouchView, mDownTouchPosition, id);
    }

    if (!handled) {
      mContextMenuInfo = new AdapterContextMenuInfo(view, position, id);
      handled = super.showContextMenuForChild(this);
    }

    if (handled) {
      performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
    }

    return handled;
  }

  @Override
  public boolean dispatchKeyEvent(KeyEvent event) {
    // Gallery steals all key events
    return event.dispatch(this, null, null);
  }

  /**
   * Handles left, right, and clicking
   * 
   * @see android.view.View#onKeyDown
   */
  @Override
  public boolean onKeyDown(int keyCode, KeyEvent event) {
    switch (keyCode) {

      case KeyEvent.KEYCODE_DPAD_LEFT:
        if (movePrevious()) {
          playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
        }
        return true;

      case KeyEvent.KEYCODE_DPAD_RIGHT:
        if (moveNext()) {
          playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
        }
        return true;

      case KeyEvent.KEYCODE_DPAD_CENTER:
      case KeyEvent.KEYCODE_ENTER:
        mReceivedInvokeKeyDown = true;
        // fallthrough to default handling
    }

    return super.onKeyDown(keyCode, event);
  }

  @Override
  public boolean onKeyUp(int keyCode, KeyEvent event) {
    switch (keyCode) {
      case KeyEvent.KEYCODE_DPAD_CENTER:
      case KeyEvent.KEYCODE_ENTER: {

        if (mReceivedInvokeKeyDown) {
          if (mItemCount > 0) {

            dispatchPress(mSelectedChild);
            postDelayed(new Runnable() {
              public void run() {
                dispatchUnpress();
              }
            }, ViewConfiguration.getPressedStateDuration());

            int selectedIndex = mSelectedPosition - mFirstPosition;
            performItemClick(getChildAt(selectedIndex), mSelectedPosition, mAdapter.getItemId(mSelectedPosition));
          }
        }

        // Clear the flag
        mReceivedInvokeKeyDown = false;

        return true;
      }
    }

    return super.onKeyUp(keyCode, event);
  }

  boolean movePrevious() {
    if (mItemCount > 0 && mSelectedPosition > 0) {
      scrollToChild(mSelectedPosition - mFirstPosition - 1);
      return true;
    } else {
      return false;
    }
  }

  boolean moveNext() {
    if (mItemCount > 0 && mSelectedPosition < mItemCount - 1) {
      scrollToChild(mSelectedPosition - mFirstPosition + 1);
      return true;
    } else {
      return false;
    }
  }

  void scrollToChild(int i) {
    CarouselItemTag tag = (CarouselItemTag) getChildAt(i).getTag(TAG_KEY);
    float angle = tag.angle;

    if (angle == 0)
      return;

    if (angle > 180.0f) {
      angle = -(360.0f - angle);
    }

    mFlingRunnable.startUsingDistance(angle);

  }

  @Override
  void setSelectedPositionInt(int position) {
    super.setSelectedPositionInt(position);

    // Updates any metadata we keep about the selected item.
    updateSelectedItemMetadata();
  }

  private void updateSelectedItemMetadata() {

    View oldSelectedChild = mSelectedChild;

    View child = mSelectedChild = getChildAt(mSelectedPosition - mFirstPosition);
    if (child == null) {
      return;
    }

    child.setSelected(true);
    child.setFocusable(true);

    if (hasFocus()) {
      child.requestFocus();
    }

    // We unfocus the old child down here so the above hasFocus check
    // returns true
    if (oldSelectedChild != null) {

      // Make sure its drawable state doesn't contain 'selected'
      oldSelectedChild.setSelected(false);

      // Make sure it is not focusable anymore, since otherwise arrow keys
      // can make this one be focused
      oldSelectedChild.setFocusable(false);
    }

  }

  @Override
  protected int getChildDrawingOrder(int childCount, int i) {
    // Sort Carousel items by z coordinate in reverse order
    ArrayList<CarouselItemTag> sl = new ArrayList<CarouselItemTag>();
    for (int j = 0; j < childCount; j++) {
      CarouselItemTag tag = (CarouselItemTag) getChildAt(j).getTag(TAG_KEY);
      if (i == 0){
        tag.hasDrawn = false;
      }
      sl.add(tag);
    }

    Collections.sort(sl);

    // Get first undrawn item in array and get result index
    int idx = 0;

    for (CarouselItemTag tag : sl) {
      if (!tag.hasDrawn) {
        tag.hasDrawn = true;
        idx = tag.position;
        break;
      }
    }

    return idx;
  }

  @Override
  protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
    super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);

    /*
     * The gallery shows focus by focusing the selected item. So, give focus to
     * our selected item instead. We steal keys from our selected item
     * elsewhere.
     */
    if (gainFocus && mSelectedChild != null) {
      mSelectedChild.requestFocus(direction);
    }

  }

  private class FlingRotateRunnable implements Runnable {

    /**
     * Tracks the decay of a fling rotation
     */
    private Rotator mRotator;

    /**
     * Angle value reported by mRotator on the previous fling
     */
    private float mLastFlingAngle;

    /**
     * Constructor
     */
    public FlingRotateRunnable() {
      mRotator = new Rotator(getContext());
    }

    private void startCommon() {
      // Remove any pending flings
      removeCallbacks(this);
    }

    public void startUsingVelocity(float initialVelocity) {
      if (initialVelocity == 0)
        return;

      startCommon();

      mLastFlingAngle = 0.0f;

      mRotator.fling(initialVelocity);

      post(this);
    }

    public void startUsingDistance(float deltaAngle) {
      if (deltaAngle == 0)
        return;

      startCommon();

      mLastFlingAngle = 0;
      synchronized (this) {
        mRotator.startRotate(0.0f, deltaAngle, mAnimationDuration);
      }
      post(this);
    }

    public void stop(boolean scrollIntoSlots) {
      removeCallbacks(this);
      endFling(scrollIntoSlots);
    }

    private void endFling(boolean scrollIntoSlots) {
      /*
       * Force the scroller's status to finished (without setting its position
       * to the end)
       */
      synchronized (this) {
        mRotator.forceFinished(true);
      }

      if (scrollIntoSlots)
        scrollIntoSlots();
    }

    public void run() {
      if (Carousel.this.getChildCount() == 0) {
        endFling(true);
        return;
      }

      mShouldStopFling = false;

      final Rotator rotator;
      final float angle;
      boolean more;
      synchronized (this) {
        rotator = mRotator;
        more = rotator.computeAngleOffset();
        angle = rotator.getCurrAngle();
      }

      // Flip sign to convert finger direction to list items direction
      // (e.g. finger moving down means list is moving towards the top)
      float delta = mLastFlingAngle - angle;

      // ////// Shoud be reworked
      trackMotionScroll(delta);

      if (more && !mShouldStopFling) {
        mLastFlingAngle = angle;
        post(this);
      } else {
        mLastFlingAngle = 0.0f;
        endFling(true);
      }

    }

  }

  /**
   * Gallery extends LayoutParams to provide a place to hold current
   * Transformation information along with previous position/transformation
   * info.
   * 
   */
  public static class LayoutParams extends ViewGroup.LayoutParams {
    public LayoutParams(Context c, AttributeSet attrs) {
      super(c, attrs);
    }

    public LayoutParams(int w, int h) {
      super(w, h);
    }

    public LayoutParams(ViewGroup.LayoutParams source) {
      super(source);
    }
  }

  class CarouselItemTag implements Comparable<CarouselItemTag>{
    int position;
    float x;
    float y;
    float scale;
    float angle;
    
    boolean hasDrawn = true;

    @Override
    public int compareTo(CarouselItemTag another) {
      return (y - another.y) > 0 ? 1 : -1;
    }
  }
}
